import type { Agent } from "./Agent";
import * as Debug from "./Debug";
import type { AZone } from "./AZone";
import type { AObject } from "./AObject";
import type { HistoryEntry, SentenceContent } from "./NarrativeBase";
import { U } from "./Util";

export type ScheduledProcess = 
    Generator<ScheduleControlRequest, unknown, ScheduleResponse>;
export type TypedScheduledProcess<T> = 
    Generator<ScheduleControlRequest, T, ScheduleResponse>;

export type ScheduleControlRequest = 
      SchedulePassTimeRequest | ScheduleInterruptRequest | ScheduleGlobalInputRequest;

export type SchedulePassTimeRequest = {
    type: 'timePass', 
    value: number,
    isInterrupt?(int: ScheduleInterruptRequest): boolean
}

export type ScheduleInterruptRequest = {
    type: 'interrupt', 
    from: AObject | Agent | undefined,
    report: HistoryEntry,
    affectRange: Set<AZone>,
    severity: number // 1~3
}

export type ScheduleGlobalInputRequest = {
    type: 'waitGlobalInput',
    prompt: string // this will be meta
}

export type ScheduleResponse = {
    success: boolean,
    interrupt?: Readonly<ScheduleInterruptRequest>, // 0~3
    userInput: string
}

type ScheduleEntry = {
    action: ScheduledProcess,
    from: AObject | Agent,
    delete: boolean,
    nextTime: number,
    isInterrupt?: (int: ScheduleInterruptRequest) => boolean,
    response?: ScheduleResponse
}

export let World = {
    day: 1,
    outdoorTemperature: 5, // unit: °C

    schedule: {
        absoluteTime: 0, // unit: second
        coroutines: [] as ScheduleEntry[], // sorted from far to near future

        addAction(act: ScheduledProcess, from: AObject | Agent) {
            // simply add at the end
            let entry: ScheduleEntry = {
                action: act, 
                from: from,
                delete: false,
                nextTime: this.absoluteTime};
            this.coroutines.push(entry);
            return entry;
        },

        requireTime(t: number, 
            check?: (x: ScheduleInterruptRequest) => boolean): SchedulePassTimeRequest {
            return {type: 'timePass', value: t, isInterrupt: check}; },

        requireGlobalInput(prompt: string): ScheduleGlobalInputRequest {
            return {type: 'waitGlobalInput', prompt: prompt}; },

        issueInterrupt(level: number, from: AObject | Agent | undefined, 
            report: HistoryEntry, range: Iterable<AZone>): ScheduleInterruptRequest {
            return {
                type: 'interrupt', 
                from: from,
                severity: level, 
                report: report,
                affectRange: new Set(range)}; },
    },
}

function _cleanSchedule() {
    let newlist: ScheduleEntry[] = [];
    for (let e of World.schedule.coroutines)
        if (!e.delete) newlist.push(e);
    newlist.sort((a, b) => b.nextTime - a.nextTime); // descending
    World.schedule.coroutines = newlist;
}

function* _processControl(entry: ScheduleEntry, ctrl: ScheduleControlRequest) {
    let response: ScheduleResponse = {
        success: true,
        userInput: ''
    };
    switch (ctrl.type) {
    case 'timePass':
        Debug.assert(ctrl.value >= 0);
        World.schedule.absoluteTime = entry.nextTime;
        entry.nextTime += ctrl.value;
        entry.response = response;
        entry.isInterrupt = ctrl.isInterrupt;
        break;
    case 'interrupt':
        response.success = false;
        response.interrupt = ctrl;
        console.log('interrupt issued at', U.timeString(World.schedule.absoluteTime));

        // notify all relevent pending coroutines
        for (let c of World.schedule.coroutines) {
            if (c == entry) continue;
            if (c.from.parentZone != null
                && ctrl.affectRange.has(c.from.parentZone)
                && (!c.isInterrupt || c.isInterrupt(ctrl))) 
            {
                // reset next time
                c.response = response;
                c.nextTime = World.schedule.absoluteTime;
            }
        }
        
        // dispatch report
        Debug.unregister(ctrl.report);
        for (let z of ctrl.affectRange) {
            let ent = ctrl.report.clone();
            ent.startTime = World.schedule.absoluteTime;
            ent.endTime = ent.startTime;
            z.history.pushEntry(ent);
        }

        entry.response = {success: true, userInput: ''};
        break;
    case 'waitGlobalInput':
        response.userInput = yield ctrl.prompt;
        entry.response = response;
        break;
    }
}

export let UniversalImperatives = {
    resetScheduler() {
        for (let c of World.schedule.coroutines) {
            c.action.return(null); // is this necessary?
        }
        World.schedule.coroutines = [];
    },

    /**
     * This will run world simulation by activating all ongoing actions until
     * the time point `tt`, INCLUDING actions that are scheduled to fire AT
     * the precise time point `tt`.
     */
    *passTimeUntil(tt: number) {
        Debug.assert(tt >= World.schedule.absoluteTime);

        while (World.schedule.coroutines.length > 0) {
            let coroutines = World.schedule.coroutines,
                nearest = coroutines[coroutines.length - 1];
            if (nearest.nextTime > tt) {
                break;
            } else {
                let result = nearest.response == undefined ? 
                        nearest.action.next() : nearest.action.next(nearest.response);
                if (!result.done)
                    yield* _processControl(nearest, result.value);
                else nearest.delete = true;
            }
            _cleanSchedule();
        }
        World.schedule.absoluteTime = tt;
    }
}
export function* forcePassTime(t: number): ScheduledProcess {
    Debug.assert(t > 0);
    let target = World.schedule.absoluteTime + t;
    while (true) {
        let result = 
            yield World.schedule.requireTime(target - World.schedule.absoluteTime);
        if (result.success) return;
    }
}